package com.gotokeep.keep.composition.demo.internal;

import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMetadataRetriever;
import android.media.MediaMuxer;
import android.opengl.EGL14;
import android.opengl.EGLConfig;
import android.opengl.EGLContext;
import android.opengl.EGLDisplay;
import android.opengl.EGLExt;
import android.opengl.EGLSurface;
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.SystemClock;
import android.text.format.DateUtils;
import android.util.Log;
import android.view.Surface;

import com.gotokeep.keep.composition.demo.renderer.TransitionRender;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.concurrent.Semaphore;
import java.util.concurrent.atomic.AtomicBoolean;

import static android.support.v4.math.MathUtils.clamp;

/**
 * @author xana/cuixianming
 * @version 1.0
 * @since 2018-04-28 09:56
 */
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
public class TransitionRenderThread extends Thread implements SurfaceTexture.OnFrameAvailableListener, Handler.Callback {
    private static final String TAG = TransitionRenderThread.class.getSimpleName();
    private static final boolean VERBOSE = true;
    private static final long EXPORT_DURATION_SEC = 15;
    private AtomicBoolean shouldRender;

    private SurfaceTexture[] inputSurfaceTexture = new SurfaceTexture[2];
    private Surface[] inputSurface = new Surface[2];
    private int[] inputTexId;

    private String outputPath;
    private Surface outputSurface;

    private MediaExtractor mediaExtractor[] = new MediaExtractor[2];
    private MediaFormat videoFormat[] = new MediaFormat[2];
    private MediaCodec videoDecoder[] = new MediaCodec[2];
    private MediaFormat encodeFormat;
    private MediaCodec encoder;
    private MediaMuxer muxer;
    private boolean muxerStarted = false;
    private int muxerTrackIndex = -1;
    private MediaCodec.BufferInfo videoInfo[] = new MediaCodec.BufferInfo[]{
            new MediaCodec.BufferInfo(), new MediaCodec.BufferInfo()
    };
    private MediaCodec.BufferInfo encodeInfo = new MediaCodec.BufferInfo();

    private TransitionRender renderer;

    private EGLDisplay mEglDisplay = EGL14.EGL_NO_DISPLAY;
    private EGLContext mEglContext = EGL14.EGL_NO_CONTEXT;
    private EGLSurface mEglSurface = EGL14.EGL_NO_SURFACE;
    private EGLConfig[] configs;

    private int width = 0;
    private int height = 0;
    private int rotation = 0;
    private int frameRate = 0;
    private int keyFrameInterval = 0;
    private String filePath[] = new String[2];
    private int videoTrackIndex[] = new int[]{-1, -1};

    private RenderListener renderListener;

    private static final int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
    private static final int EGL_OPENGL_ES2_BIT = 4;
    private static final int EGL_RECORDABLE_ANDROID = 0x3142;
    private boolean isVideoEnd = false;
    private float speed = 1.0f;
    private boolean firstFrameNoticed = false;

    private static final int EXPORT_FRAME_RATE = 20;

    private boolean export = false;

    private HandlerThread encodeThread;
    private Handler encodeHandler;
    private Handler eventHandler;
    private Semaphore encodeSema = new Semaphore(0);

    public TransitionRenderThread(String[] filePath, String outputPath, SurfaceTexture outputSurfaceTexture, TransitionRender renderer,
                                  AtomicBoolean shouldRender, float speed, Handler eventHandler) {
        this.outputPath = outputPath;
        this.eventHandler = eventHandler;
        if (outputSurfaceTexture != null) {
            this.outputSurface = new Surface(outputSurfaceTexture);
        }
        this.renderer = renderer;
        this.shouldRender = shouldRender;
        this.filePath = filePath;
        this.speed = speed;

        encodeThread = new HandlerThread("TransitionThread:encode");
        encodeThread.start();
        encodeHandler = new Handler(encodeThread.getLooper(), this);
    }

    public Surface createInputSurfaces() {
        int texId[] = new int[2];
        GLES20.glGenTextures(2, texId, 0);
        for (int i = 0; i < texId.length; i++) {
            GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, texId[i]);

            GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER,
                    GLES20.GL_LINEAR);
            GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER,
                    GLES20.GL_LINEAR);

            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S,
                    GLES20.GL_REPEAT);
            GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T,
                    GLES20.GL_REPEAT);

            inputSurfaceTexture[i] = new SurfaceTexture(texId[i]);
            inputSurfaceTexture[i].setOnFrameAvailableListener(this);
            inputSurface[i] = new Surface(inputSurfaceTexture[i]);
        }

        inputTexId = texId;
        return inputSurface[0];
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    public void createOutputSurface() {
        outputSurface = encoder.createInputSurface();
        encoder.start();
    }

    private void initDecoder(int index, String[] filePath) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            throw new RuntimeException("系统版本需要 4.3 以上");
        }
        File mediaFile = new File(filePath[index]);
        if (!mediaFile.exists()) {
            throw new RuntimeException("文件不存在");
        }
        mediaExtractor[index] = new MediaExtractor();
        try {
            mediaExtractor[index].setDataSource(filePath[index]);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        videoTrackIndex[index] = -1;
        for (int i = 0; i < mediaExtractor[index].getTrackCount(); i++) {
            MediaFormat format = mediaExtractor[index].getTrackFormat(i);
            String mime = format.getString(MediaFormat.KEY_MIME);
            if (mime.startsWith("video/")) {
                videoTrackIndex[index] = i;
                videoFormat[index] = format;
                break;
            }
        }

        mediaExtractor[index].selectTrack(videoTrackIndex[index]);
        width = videoFormat[index].getInteger(MediaFormat.KEY_WIDTH);
        height = videoFormat[index].getInteger(MediaFormat.KEY_HEIGHT);
        if (videoFormat[index].containsKey("crop-left") && videoFormat[index].containsKey("crop-right")) {
            width = videoFormat[index].getInteger("crop-right") + 1 - videoFormat[index].getInteger("crop-left");
        }
        if (videoFormat[index].containsKey("crop-top") && videoFormat[index].containsKey("crop-bottom")) {
            height = videoFormat[index].getInteger("crop-bottom") + 1 - videoFormat[index].getInteger("crop-top");
        }
        rotation = getRotation(filePath[index]);
        if (eventHandler != null) {
            eventHandler.obtainMessage(RenderListener.MSG_VIDEO_SIZE_CHANGE, new VideoSizeInfo(width, height, rotation)).sendToTarget();
//            renderListener.onVideoSizeChanged(width, height, rotation);
        }
        frameRate = videoFormat[index].getInteger(MediaFormat.KEY_FRAME_RATE);
        frameRate = (frameRate == 0) ? 24 : frameRate;
        inputSurfaceTexture[index].setDefaultBufferSize(width, height);
//        keyFrameInterval = videoFormat.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL);

        String mime = videoFormat[index].getString(MediaFormat.KEY_MIME);
        try {
            videoDecoder[index] = MediaCodec.createDecoderByType(mime);
            videoDecoder[index].configure(videoFormat[index], inputSurface[index], null, 0);
            videoDecoder[index].start();
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void initEncoder(String outputPath) {
        encodeFormat = MediaFormat.createVideoFormat("video/avc", 960, 540);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            encodeFormat.setInteger(MediaFormat.KEY_PROFILE, MediaCodecInfo.CodecProfileLevel.AVCProfileHigh);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            encodeFormat.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel52);
        }
        encodeFormat.setInteger(MediaFormat.KEY_FRAME_RATE, EXPORT_FRAME_RATE);
        encodeFormat.setInteger(MediaFormat.KEY_BIT_RATE, 2500000);
        encodeFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 30);
        encodeFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
        try {
            encoder = MediaCodec.createEncoderByType("video/avc");
            encoder.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);

            muxer = new MediaMuxer(outputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void initGL() {

        mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
        if (mEglDisplay == EGL14.EGL_NO_DISPLAY) {
            throw new RuntimeException("eglGetDisplay failed : " +
                    GLUtils.getEGLErrorString(EGL14.eglGetError()));
        }

        int[] version = new int[2];
        if (!EGL14.eglInitialize(mEglDisplay, version, 0, version, 1)) {
            throw new RuntimeException("eglInitialize failed : " +
                    GLUtils.getEGLErrorString(EGL14.eglGetError()));
        }

        int[] configAttribs = {
                EGL14.EGL_BUFFER_SIZE, 32,
                EGL14.EGL_ALPHA_SIZE, 8,
                EGL14.EGL_BLUE_SIZE, 8,
                EGL14.EGL_GREEN_SIZE, 8,
                EGL14.EGL_RED_SIZE, 8,
                EGL14.EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
                EGL_RECORDABLE_ANDROID, 1,
                EGL14.EGL_NONE
        };

        int[] numConfigs = new int[1];
        configs = new EGLConfig[1];
        if (!EGL14.eglChooseConfig(mEglDisplay, configAttribs, 0, configs, 0, 1, numConfigs, 0)) {
            throw new RuntimeException("eglChooseConfig failed : " +
                    GLUtils.getEGLErrorString(EGL14.eglGetError()));
        }

        int[] contextAttribs = {
                EGL14.EGL_CONTEXT_CLIENT_VERSION, 2,
                EGL14.EGL_NONE
        };
        mEglContext = EGL14.eglCreateContext(mEglDisplay, configs[0], EGL14.EGL_NO_CONTEXT, contextAttribs, 0);
        mEglSurface = EGL14.eglCreateWindowSurface(mEglDisplay, configs[0], outputSurface, null, 0);
        if (mEglSurface == EGL14.EGL_NO_SURFACE || mEglContext == EGL14.EGL_NO_CONTEXT) {
            int error = EGL14.eglGetError();
            if (error == EGL14.EGL_BAD_NATIVE_WINDOW) {
                throw new RuntimeException("eglCreateWindowSurface returned  EGL_BAD_NATIVE_WINDOW. ");
            }
            throw new RuntimeException("eglCreateWindowSurface failed : " +
                    GLUtils.getEGLErrorString(EGL14.eglGetError()));
        }

        if (!EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
            throw new RuntimeException("eglMakeCurrent failed : " +
                    GLUtils.getEGLErrorString(EGL14.eglGetError()));
        }
    }

    private void init(String[] filePath, String outputPath) {

        if (export) {
            initEncoder(outputPath);
            createOutputSurface();
        }
        initGL();
        createInputSurfaces();
        for (int i = 0; i < filePath.length; i++) {
            initDecoder(i, filePath);
        }
    }

    private void destroyGL() {
        GLES20.glDeleteTextures(inputTexId.length, inputTexId, 0);

        EGL14.eglMakeCurrent(mEglDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE,
                EGL14.EGL_NO_CONTEXT);
        EGL14.eglDestroyContext(mEglDisplay, mEglContext);
        EGL14.eglDestroySurface(mEglDisplay, mEglSurface);
        EGL14.eglReleaseThread();
        EGL14.eglTerminate(mEglDisplay);

        outputSurface.release();
        mEglDisplay = EGL14.EGL_NO_DISPLAY;
        mEglContext = EGL14.EGL_NO_CONTEXT;
        mEglSurface = EGL14.EGL_NO_SURFACE;
        outputSurface = null;
    }

    private void destroyDecoder() {
        for (int i = 0; i < videoDecoder.length; i++) {
            videoDecoder[i].stop();
            videoDecoder[i].release();
            videoDecoder[i] = null;
        }

        for (int i = 0; i < mediaExtractor.length; i++) {
            mediaExtractor[i].release();
            mediaExtractor[i] = null;
        }
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void destoryEncoder() {
        if (muxer != null) {
            muxer.stop();
            muxer.release();
            muxer = null;
        }

        if (encoder != null) {
            encoder.stop();
            encoder.release();
            encoder = null;
        }
    }

    private void release() {
        destroyDecoder();
        for (int i = 0; i < inputSurfaceTexture.length; i++) {
            if (inputSurface[i] != null) {
                inputSurface[i].release();
            }
            inputSurface[i] = null;
            inputSurfaceTexture[i].detachFromGLContext();
            inputSurfaceTexture[i] = null;
        }
        destroyGL();
        if (export) {
            destoryEncoder();
        }
    }

    private int getRotation(String path) {
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        try {
            retriever.setDataSource(path);
            String rotation = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION);
            return Integer.parseInt(rotation);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return 0;
    }

    private boolean generateFrame(int index) {
        boolean decoded = false;
        while (!decoded && !isVideoEnd) {
            int inputIndex = videoDecoder[index].dequeueInputBuffer(1000);
            if (inputIndex >= 0) {
                ByteBuffer buffer = getInputBuffer(videoDecoder[index], inputIndex);
                buffer.clear();
                mediaExtractor[index].selectTrack(videoTrackIndex[index]);
                int ret = mediaExtractor[index].readSampleData(buffer, 0);
                if (ret < 0) {
                    videoDecoder[index].queueInputBuffer(inputIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                    isVideoEnd = true;
                } else {
                    long videoTimestamp = mediaExtractor[index].getSampleTime();
                    videoDecoder[index].queueInputBuffer(inputIndex, 0, ret, videoTimestamp, mediaExtractor[index].getSampleFlags());
                    mediaExtractor[index].advance();
                }
            }
            int outputIndex = videoDecoder[index].dequeueOutputBuffer(videoInfo[index], 1000);
            switch (outputIndex) {
                case MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED:
                    videoDecoder[index].setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
                    break;
                case MediaCodec.INFO_OUTPUT_FORMAT_CHANGED:
                    videoDecoder[index].setVideoScalingMode(MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
                    break;
                case MediaCodec.INFO_TRY_AGAIN_LATER:
                    break;
                default:
                    videoDecoder[index].releaseOutputBuffer(outputIndex, true);
                    decoded = true;
                    break;
            }
        }
        if (!firstFrameNoticed && eventHandler != null) {
            eventHandler.sendEmptyMessage(RenderListener.MSG_FIRST_FRAME_RENDERED);
//            renderListener.onFirstFrameRendered();
            firstFrameNoticed = true;
        }
        return decoded && !isVideoEnd;
    }

    private ByteBuffer getInputBuffer(MediaCodec decoder, int inputIndex) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return decoder.getInputBuffer(inputIndex);
        } else {
            return decoder.getInputBuffers()[inputIndex];
        }
    }

    private ByteBuffer getOutputBuffer(MediaCodec decoder, int outputIndex) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            return decoder.getOutputBuffer(outputIndex);
        } else {
            return decoder.getOutputBuffers()[outputIndex];
        }
    }

    @TargetApi(Build.VERSION_CODES.M)
    @Override
    public void run() {
        init(filePath, outputPath);
        if (renderer != null) {
            renderer.onInit(inputTexId);
        }

        long startMs = SystemClock.elapsedRealtime();
        long lastTime = startMs;
        isVideoEnd = false;
        long presentationTimeUs = 0;
        int frameIndex = 0;
        final long ONE_BILLION = 1000000;
        while (shouldRender != null && shouldRender.get() != false) {
            lastTime = SystemClock.elapsedRealtime();
            long playTime = lastTime - startMs;
            if (export) {
                presentationTimeUs = frameIndex++ * ONE_BILLION / EXPORT_FRAME_RATE;
            }
            if (presentationTimeUs / 1000 < playTime || export) {
                if (export) {
                    encodeHandler.sendEmptyMessage(0);
                }
                if (eventHandler != null) {
                    eventHandler.obtainMessage(RenderListener.MSG_FRAME_TIME, presentationTimeUs).sendToTarget();
//                    renderListener.onFrameTime(presentationTimeUs / 1000);
                }
                if (export || videoInfo[0].presentationTimeUs / 1000 >= 0 && videoInfo[0].presentationTimeUs / 1000 < 4 * DateUtils.SECOND_IN_MILLIS) {
                    generateFrame(0);
                    if (!export) {
                        presentationTimeUs = videoInfo[0].presentationTimeUs;
                    }
                }
                if (export || presentationTimeUs / 1000 >= 2 * DateUtils.SECOND_IN_MILLIS) {
                    generateFrame(1);
                    if (!export) {
                        presentationTimeUs = videoInfo[1].presentationTimeUs + 2 * 1000000;
//                    presentationTimeUs = videoInfo[1].presentationTimeUs;
                    }
                }

                if (export) {
                    try {
                        encodeSema.acquire();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                inputSurfaceTexture[0].updateTexImage();
                inputSurfaceTexture[1].updateTexImage();
                if (renderer != null) {
                    float alpha = clamp((float) (playTime - 2000) / 2000, 0f, 1.0f);
                    renderer.setAlpha(alpha);
                    renderer.onDrawFrame();
                    Log.d(TAG, "Render frame");
                    if (export) {
                        if (VERBOSE) Log.d(TAG, "sending frame " + frameIndex + " to encoder");
                        EGLExt.eglPresentationTimeANDROID(mEglDisplay, mEglSurface, presentationTimeUs * 1000);
                    }
                    EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
                } else {
                    break;
                }
            }
            if (presentationTimeUs / 1000 > EXPORT_DURATION_SEC * DateUtils.SECOND_IN_MILLIS) {
                break;
            }
        }
        if (eventHandler != null) {
            eventHandler.sendEmptyMessage(RenderListener.MSG_VIDEO_END);
//            renderListener.onVideoEnd();
        }
        if (export && encoder != null) {
            drainEncoder(true);
        }
        Log.d(TAG, "Render completed.");

        if (renderer != null) {
            renderer.onRelease();
        }

        release();
    }

    @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
    private void drainEncoder(boolean endOfStream) {
        if (endOfStream) {
            Log.d(TAG, "drainEncoder: End of Stream");
            encoder.signalEndOfInputStream();
        }
        while (true) {
            int encoderIndex = encoder.dequeueOutputBuffer(encodeInfo, 10000);
            if (encoderIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                Log.d(TAG, "drainEncoder: try later");
                if (!endOfStream) {
                    encodeSema.release();
                    break;
                }else {
                    if (VERBOSE) Log.d(TAG, "no output available, spinning to await EOS");
                }
            } else if (encoderIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                Log.d(TAG, "drainEncoder: output buffers changed");
            } else if (encoderIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                if (muxerStarted) {
                    throw new RuntimeException("format changed twice");
                }
                MediaFormat newFormat = encoder.getOutputFormat();
                Log.d(TAG, "drainEncoder: encoder output format changed: " + newFormat);
                muxerTrackIndex = muxer.addTrack(newFormat);
                muxer.start();
                muxerStarted = true;
            } else if (encoderIndex < 0) {
                Log.w(TAG, "drainEncoder: unexpected result form encoder#dequeueOutputBuffer: " + encoderIndex);
            } else {
                ByteBuffer encodedData = getOutputBuffer(encoder, encoderIndex);
                Log.d(TAG, "drainEncoder: encodedDataInfo = " + getInfoString(encodeInfo));
                if ((encodeInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    encodeInfo.size = 0;
                }

                if (encodeInfo.size != 0) {
                    if (!muxerStarted) {
                        throw new RuntimeException("muxer has not started");
                    }

                    encodedData.position(encodeInfo.offset);
                    encodedData.limit(encodeInfo.offset + encodeInfo.size);
                    muxer.writeSampleData(muxerTrackIndex, encodedData, encodeInfo);
                    Log.d(TAG, "drainEncoder: send " + encodeInfo.size + " bytes to muxer");
                }

                encoder.releaseOutputBuffer(encoderIndex, false);

                if ((encodeInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    if (!endOfStream) {
                        Log.w(TAG, "reached end of stream unexpectedly");
                    }
                    encodeSema.release();
                    break;
                }
            }
        }
    }

    private String getInfoString(MediaCodec.BufferInfo encodeInfo) {
        return String.format(Locale.CHINA, "BufferInfo { offset = %d, size = %d, presentationTimeUs = %d }",
                encodeInfo.offset, encodeInfo.size, encodeInfo.presentationTimeUs);
    }

    @Override
    public void onFrameAvailable(SurfaceTexture surfaceTexture) {
        Log.d(TAG, "onFrameAvailable: ");
    }

    public RenderListener getRenderListener() {
        return renderListener;
    }

    public void setRenderListener(RenderListener renderListener) {
        this.renderListener = renderListener;
    }

    public void setExport(boolean export) {
        this.export = export;
    }

    @Override
    public boolean handleMessage(Message msg) {
        drainEncoder(false);
        return false;
    }

    public interface RenderListener {
        int MSG_VIDEO_SIZE_CHANGE = 0;
        int MSG_VIDEO_END = 1;
        int MSG_FRAME_TIME = 2;
        int MSG_FIRST_FRAME_RENDERED = 3;

        void onVideoSizeChanged(int width, int height, int rotation);

        void onVideoEnd();

        void onFrameTime(long timeUs);

        void onFirstFrameRendered();
    }

    public final class VideoSizeInfo {
        public int width;
        public int height;
        public int rotation;

        public VideoSizeInfo(int width, int height, int rotation) {
            this.width = width;
            this.height = height;
            this.rotation = rotation;
        }
    }
}
